a tool for shared writing and social publishing
1import { useReplicache } from "src/replicache";
2import React, { useEffect, useState } from "react";
3import { getShareLink } from "./getShareLink";
4import { useEntitySetContext } from "components/EntitySetProvider";
5import { useSmoker } from "components/Toast";
6import { Menu, MenuItem } from "components/Menu";
7import { ActionButton } from "components/ActionBar/ActionButton";
8import useSWR from "swr";
9import LoginForm from "app/login/LoginForm";
10import { CustomDomainMenu } from "./DomainOptions";
11import { useIdentityData } from "components/IdentityProvider";
12import {
13 useLeafletDomains,
14 useLeafletPublicationData,
15} from "components/PageSWRDataProvider";
16import { ShareSmall } from "components/Icons/ShareSmall";
17import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL";
18import { AtUri } from "@atproto/syntax";
19import { useIsMobile } from "src/hooks/isMobile";
20
21export type ShareMenuStates = "default" | "login" | "domain";
22
23export let useReadOnlyShareLink = () => {
24 let { permission_token, rootEntity } = useReplicache();
25 let entity_set = useEntitySetContext();
26 let { data: publishLink } = useSWR(
27 "publishLink-" + permission_token.id,
28 async () => {
29 if (
30 !permission_token.permission_token_rights.find(
31 (s) => s.entity_set === entity_set.set && s.create_token,
32 )
33 )
34 return;
35 let shareLink = await getShareLink(
36 { id: permission_token.id, entity_set: entity_set.set },
37 rootEntity,
38 );
39 return shareLink?.id;
40 },
41 );
42 return publishLink;
43};
44
45export function ShareOptions() {
46 let [menuState, setMenuState] = useState<ShareMenuStates>("default");
47 let { data: pub } = useLeafletPublicationData();
48 let isMobile = useIsMobile();
49
50 return (
51 <Menu
52 asChild
53 side={isMobile ? "top" : "right"}
54 align={isMobile ? "center" : "start"}
55 className="max-w-xs"
56 onOpenChange={() => {
57 setMenuState("default");
58 }}
59 trigger={
60 <ActionButton
61 icon=<ShareSmall />
62 secondary
63 label={`Share ${pub ? "Draft" : ""}`}
64 />
65 }
66 >
67 {menuState === "login" ? (
68 <div className="px-3 py-1">
69 <LoginForm text="Save your Leaflets and access them on multiple devices!" />
70 </div>
71 ) : menuState === "domain" ? (
72 <CustomDomainMenu setShareMenuState={setMenuState} />
73 ) : (
74 <ShareMenu
75 setMenuState={setMenuState}
76 domainConnected={false}
77 isPub={!!pub}
78 />
79 )}
80 </Menu>
81 );
82}
83
84const ShareMenu = (props: {
85 setMenuState: (state: ShareMenuStates) => void;
86 domainConnected: boolean;
87 isPub?: boolean;
88}) => {
89 let { permission_token } = useReplicache();
90 let { data: pub, normalizedDocument } = useLeafletPublicationData();
91
92 let postLink =
93 pub?.documents && normalizedDocument
94 ? getDocumentURL(
95 normalizedDocument,
96 pub.documents.uri,
97 pub?.publications || null,
98 )
99 : null;
100 let publishLink = useReadOnlyShareLink();
101 let [collabLink, setCollabLink] = useState<null | string>(null);
102 useEffect(() => {
103 // strip leading '/' character from pathname
104 setCollabLink(window.location.pathname.slice(1));
105 }, []);
106 let { data: domains } = useLeafletDomains();
107
108 return (
109 <>
110 <ShareButton
111 text={`Share ${postLink ? "Draft" : ""} Edit Link`}
112 subtext=""
113 smokerText="Edit link copied!"
114 id="get-edit-link"
115 link={collabLink}
116 />
117 <ShareButton
118 text={`Share ${postLink ? "Draft" : ""} View Link`}
119 subtext=<>
120 {domains?.[0] ? (
121 <>
122 This Leaflet is published on{" "}
123 <span className="italic underline">
124 {domains[0].domain}
125 {domains[0].route}
126 </span>
127 </>
128 ) : (
129 ""
130 )}
131 </>
132 smokerText="View link copied!"
133 id="get-view-link"
134 fullLink={
135 domains?.[0]
136 ? `https://${domains[0].domain}${domains[0].route}`
137 : undefined
138 }
139 link={publishLink || ""}
140 />
141 {postLink && (
142 <>
143 <hr className="border-border-light" />
144
145 <ShareButton
146 text="Share Published Link"
147 subtext=""
148 smokerText="Post link copied!"
149 id="get-post-link"
150 fullLink={postLink.includes("http") ? postLink : undefined}
151 link={postLink}
152 />
153 </>
154 )}
155 {!props.isPub && (
156 <>
157 <hr className="border-border mt-1" />
158 <DomainMenuItem setMenuState={props.setMenuState} />
159 </>
160 )}
161 </>
162 );
163};
164
165export const ShareButton = (props: {
166 text: React.ReactNode;
167 subtext?: React.ReactNode;
168 smokerText: string;
169 id: string;
170 link: null | string;
171 fullLink?: string;
172 className?: string;
173}) => {
174 let smoker = useSmoker();
175
176 return (
177 <MenuItem
178 id={props.id}
179 onSelect={(e) => {
180 e.preventDefault();
181 let rect = document.getElementById(props.id)?.getBoundingClientRect();
182 if (props.link || props.fullLink) {
183 navigator.clipboard.writeText(
184 props.fullLink
185 ? props.fullLink
186 : `${location.protocol}//${location.host}/${props.link}`,
187 );
188 smoker({
189 position: {
190 x: rect ? rect.left + (rect.right - rect.left) / 2 : 0,
191 y: rect ? rect.top + 26 : 0,
192 },
193 text: props.smokerText,
194 });
195 }
196 }}
197 >
198 <div className={`group/${props.id} ${props.className} leading-snug`}>
199 {props.text}
200
201 {props.subtext && (
202 <div className={`text-sm font-normal text-tertiary`}>
203 {props.subtext}
204 </div>
205 )}
206 </div>
207 </MenuItem>
208 );
209};
210
211const DomainMenuItem = (props: {
212 setMenuState: (state: ShareMenuStates) => void;
213}) => {
214 let { identity } = useIdentityData();
215 let { data: domains } = useLeafletDomains();
216
217 if (identity === null)
218 return (
219 <div className="text-tertiary font-normal text-sm px-3 py-1">
220 <button
221 className="text-accent-contrast hover:font-bold"
222 onClick={() => {
223 props.setMenuState("login");
224 }}
225 >
226 Log In
227 </button>{" "}
228 to publish on a custom domain!
229 </div>
230 );
231 else
232 return (
233 <>
234 {domains?.[0] ? (
235 <button
236 className="px-3 py-1 text-accent-contrast text-sm hover:font-bold w-fit text-left"
237 onMouseDown={() => {
238 props.setMenuState("domain");
239 }}
240 >
241 Edit custom domain
242 </button>
243 ) : (
244 <MenuItem
245 className="font-normal text-tertiary text-sm"
246 onSelect={(e) => {
247 e.preventDefault();
248 props.setMenuState("domain");
249 }}
250 >
251 Publish on a custom domain
252 </MenuItem>
253 )}
254 </>
255 );
256};